✨ feat: MCP Elicitation Support (URL-Exception -32042 Retry + Form/URL elicitation/create)#69
✨ feat: MCP Elicitation Support (URL-Exception -32042 Retry + Form/URL elicitation/create)#69TomasPalsson wants to merge 7 commits into
Conversation
…elicitation/create)
Adds MCP elicitation to cover two wire mechanisms needed against real-world
servers, primarily our AgentCore-hosted MCP gateway:
- URL-exception mode (2025-11-25, primary): AgentCore's gateway returns
JSON-RPC error -32042 (ErrorCode.UrlElicitationRequired) on `tools/call`
when a tool needs out-of-band user authorization (e.g. a downstream OAuth
consent screen the gateway can't broker inline). MCPManager.callTool now
catches this (matching both `McpError`/`UrlElicitationRequiredError`
instances and raw `{code: -32042}` objects from gateway proxies that don't
preserve the SDK class), emits an `on_elicitation` SSE event so the chat UI
renders an authorization card, and suspends via FlowStateManager exactly
like the existing OAuth "visit a URL, get resumed" flow. When the user
clicks "I've authorized — continue" (POST /api/mcp/elicitation/:flowId,
action=complete), the SAME tools/call is retried once and its result
becomes the tool result. Cancelling or a 10-minute flow timeout fails the
tool call with the elicitation message + URL folded into the error text,
so it still surfaces in-band to the model/user.
- Form + URL request mode (2025-06-18/2025-11-25 elicitation/create,
secondary): declares `capabilities.elicitation = { form: {}, url: {} }` on
the MCP client and registers a request handler via
`MCPConnection.setElicitationHandler`, so servers can also drive elicitation
as a server-initiated request rather than a tools/call exception. Form-mode
requests render field inputs (string/number/boolean/enum) and resolve with
action accept/decline; url-mode requests render the same authorization card.
Shared plumbing: a new `mcp_elicit` flow type on the existing
FlowStateManager singleton; a new `POST /api/mcp/elicitation/:flowId` route
with per-user ownership enforcement (elicitation flow IDs embed the userId
directly, unlike OAuth's one-per-server IDs, since elicitation is scoped per
tool invocation); a new `ElicitationContent`/`on_elicitation` content type
and SSE event; an `ElicitationForm` component covering both the form and
URL-card variants; and an `mcpSettings.elicitationMode` config flag forwarded
to the client.
Adapted from a fork patch (form-mode only, against an older LibreChat base)
— rebuilt the MCPManager/connection wiring against the current OBO-aware
`callTool`, and added the URL-exception path and URL elicitation/create
variant from scratch, since neither existed in the fork or upstream.
- Remove the dead mcpSettings.elicitationMode flag: dropped it from configSchema and TStartupConfig (data-provider) and from the /api/config forwarding block. It had no client consumer; no schema test referenced it. - Fix a per-call elicitation handler leak on cached connections: MCPConnection.setElicitationHandler now returns a disposer (removes the elicitation/create request handler), and MCPManager.callTool clears it in finally so a cached connection can't fire a stale closure (old toolName/signal/flow) on a later request. - Remove two unsafe SDK-boundary casts in the elicitation handler by narrowing the params union on params.mode instead of casting. - Name the handler param union once (ElicitationCreateParams) instead of duplicating it in the signature and the cast; derive the removed method literal from ElicitRequestSchema (no magic string). - Centralize the FlowStateManager type-erasure in one audited helper, asElicitationFlowManager(), documenting why the singleton's generic is erased for elicitation flows; no more scattered `as unknown as` double-casts at the call sites. - URI-encode every variable segment of an elicitation flow ID (userId, serverName, toolName — server/tool names are config/user-derived) and decode them in parse, so a `:` inside a segment can no longer skew the parsed diagnostic fields. Ownership was already sound; parse output is now always faithful. - Add regression tests: the handler is cleared once the tool call settles, and flow IDs round-trip (including a colon inside a segment and tenant-scoped IDs).
Registration on a shared cached connection is last-writer-wins (the protocol offers no call correlation for elicitation/create), but an earlier tools/call settling must not tear down a later call's live handler: the disposer now only unregisters if its own registration is still current, and is idempotent.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
CI note: the |
…rt errors Observed live against AgentCore Gateway: it returns JSON-RPC errors with a non-2xx HTTP status, so the SDK's streamable-HTTP transport throws a StreamableHTTPError (code = HTTP status, body embedded in the message) instead of an McpError with code -32042 — the elicitation catch never matched and tool calls failed generically. extractUrlElicitation() now handles both wire shapes: protocol-level McpError/UrlElicitationRequiredError, and the HTTP-wrapped body parsed out of the transport error message. Tests pin the exact observed wire format, plus non-elicitation and unparseable-body negatives.
- extractUrlElicitation: parse embedded JSON-RPC bodies regardless of whitespace/key order instead of matching a byte-exact code literal - connection: do not treat a 401-wrapped -32042 elicitation as an OAuth failure; the spurious background reconnect raced the elicitation retry and left a stale gateway session (-32600) - thread serverName/toolName through the on_elicitation payload - add AgentCore-shaped mock elicitation server (401-wrapped and 200 JSON-RPC modes, /consent + /reset) and an integration suite driving the real client stack through elicit-consent-retry end to end
- shield-headed header with requesting server/tool identity - staged CTA: one primary action at a time (open page, then confirm) - footprint-stable per-button loading, error line on failed responses, aria-live resolved state, full field-level error ARIA wiring - consolidate error styling onto semantic destructive tokens
|
Superseded by #70, which carries the same feature from a personal fork (head on |
Summary
Adds full MCP elicitation client support, primarily so AgentCore Gateway MCP_SERVER targets can do per-user 3LO OAuth mid-tool-call (apro tomas sandbox, spec 050):
tools/callrejected with JSON-RPC-32042(UrlElicitationRequired) now renders an in-chat authorization card (link opens in a new tab) and suspends viaFlowStateManager(same pattern as MCP OAuth). "I've authorized — continue" retries the same call exactly once; cancel/10-min timeout fails in-band with the message + URL preserved.elicitation/create(2025-06-18/2025-11-25): declarescapabilities.elicitation = { form: {}, url: {} }; form requests render typed fields (string/number/boolean/enum), URL requests render the authorization card.POST /api/mcp/elicitation/:flowIdcompletion route with per-user ownership (userId embedded in flow IDs; URI-encoded segments).Testing
packages/apiMCP suites: 68 passed (incl. -32042 catch/retry, flow-ID round-trips, disposal guard); full mcp dir 45 suites green pre-rebaseclient: ElicitationForm 14 + Part 5; SSE/Content dirs 618 passedtscclean on packages/api, data-provider, client; eslint cleanHow to test
github-mcp___list_created_prs→ expect the authorization card (server must be inELICITATION_MODE=url).Notes
.github-pr-draft.md(untracked scratch) — this org PR intentionally ships the full feature for internal use.🤖 Generated with Claude Code